package com.tmt.im.wallet.service;

import com.tmt.im.common.enums.ChannelType;
import com.tmt.im.common.enums.WalletEventType;
import com.tmt.im.common.pojo.*;
import com.tmt.im.jpa.entity.SystemParam;
import com.tmt.im.jpa.repository.SystemParamRepo;
import com.tmt.im.wallet.config.beans.CurrentServiceProperties;
import com.tmt.im.wallet.config.beans.PayProperties;
import com.tmt.im.wallet.controler.WithdrawController;
import com.tmt.im.wallet.controler.vo.ApplyWithdrawRequest;
import com.tmt.im.wallet.controler.vo.ConfirmWithdrawRequest;
import com.tmt.im.wallet.helper.IgniteQueryHelper;
import com.tmt.im.wallet.helper.WithdrawHelper;
import com.tmt.springboot.common.helper.IdSnowFlake;
import com.tmt.springboot.common.restful.ExecuteResult;
import com.tmt.springboot.common.restful.IExecuteResult;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.http.Method;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteSemaphore;
import org.apache.ignite.transactions.Transaction;
import org.apache.ignite.transactions.TransactionConcurrency;
import org.apache.ignite.transactions.TransactionIsolation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.net.URL;
import java.security.Principal;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Description TODO
 * @Author wuyi
 * @Date 2024/10/20 14:07
 * @Version 1.0
 **/
@Slf4j
@Service
public class WithdrawHistoryService extends BaseService {

    private final ApplicationContext applicationContext;

    private final String bucket;

    private final String apiUrlPrefix;

    private final CurrentServiceProperties serviceProperties;

    private final PayProperties payProperties;

    private final RestTemplate restTemplate;

    private final SystemParamRepo systemParamRepo;

    private final MinioClient minioClient;

    public WithdrawHistoryService(Ignite ignite,
                                  ApplicationContext applicationContext,
                                  @Value("${minio.bucket}")
                                  String bucket,
                                  @Value("${api-url-prefix}")
                                  String apiUrlPrefix,
                                  CurrentServiceProperties serviceProperties,
                                  PayProperties payProperties,
                                  RestTemplate restTemplate,
                                  SystemParamRepo systemParamRepo,
                                  MinioClient minioClient) {
        super(ignite);
        this.applicationContext = applicationContext;
        this.bucket = bucket;
        this.apiUrlPrefix = apiUrlPrefix;
        this.serviceProperties = serviceProperties;
        this.payProperties = payProperties;
        this.restTemplate = restTemplate;
        this.systemParamRepo = systemParamRepo;
        this.minioClient = minioClient;
    }

    public IExecuteResult<Boolean> applyWithdraw(Long currentUserId, ApplyWithdrawRequest request, Principal principal) {
        IMUserPoJo userPoJo = userCache.get(currentUserId);
        assert userPoJo != null;

        WithdrawBindingParamPoJo paramPoJo = getBindingParam(currentUserId, request.getType());
        if (paramPoJo == null) {
            return ExecuteResult.failure("未找到类型匹配的提现绑定参数", null);
        }

        //计算提现手续费。
        SystemParam param = systemParamRepo.findOneByParamCode("withdraw-fee");
        if (param == null) {
            return ExecuteResult.failure("读取提现费率参数失败", null);
        }

        BigDecimal rate = null;
        try {
            rate = new BigDecimal(param.getParamValue());
        } catch (Exception ex) {
            return ExecuteResult.failure("提现费率参数值转换为数值失败", null);
        }

        Long fee = rate.longValue();
        if (rate.compareTo(BigDecimal.ONE) < 0) {
            fee = BigDecimal.valueOf(request.getAmount()).multiply(rate).longValue();
        }

        Long amount = request.getAmount() + fee;

        WalletPoJo walletPoJo = walletCache.get(userPoJo.getWalletId());
        assert walletPoJo != null;
        if (walletPoJo.getBalance() < amount) {
            return ExecuteResult.failure("账户余额不足", null);
        }

        InputStream fileData = null;
        String fileName = null;
        if (request.getType() == ChannelType.WE_CHAT) {
            fileData = getFileData(paramPoJo.getFileUri());
            if (fileData != null) {
                String[] infos = paramPoJo.getFileUri().split("/");
                fileName = infos[infos.length - 1];
            }
        }

        try (IgniteSemaphore walletSemaphore = ignite.semaphore("wallet:" + userPoJo.getWalletId(), 1, true, true)) {
            if (walletSemaphore.tryAcquire(3, TimeUnit.SECONDS)) {
                try {
                    //二次验证。
                    walletPoJo = walletCache.get(userPoJo.getWalletId());
                    assert walletPoJo != null;
                    if (walletPoJo.getBalance() < amount) {
                        return ExecuteResult.failure("账户余额不足", null);
                    }

                    String orderNo = UUID.randomUUID().toString().replace("-", "");
                    String url = getNotifyUrlPrefix();

                    //创建提现历史记录。
                    WithdrawHistoryPoJo history = WithdrawHistoryPoJo.builder()
                            .id(IdSnowFlake.getId())
                            .merchantNum(payProperties.getMerchantAccount())
                            .orderNo(orderNo)
                            .withdrawAmount(request.getAmount())
                            .withdrawFee(fee)
                            .notifyUrl(url)
                            .userId("" + currentUserId)
                            .userNick(userPoJo.getNickname())
                            .sign(null)
                            .confirmMode(0)
                            .version(0L)
                            .createTime(new Timestamp(System.currentTimeMillis()))
                            .insert(true)
                            .build();

                    //根据通道类型设置提现参数。
                    configChannelParam(request.getType(), paramPoJo, history);

                    StringBuilder error = new StringBuilder();
                    if (WithdrawHelper.applyWithdraw(history, fileName, fileData, restTemplate, payProperties, error)) {
                        try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.OPTIMISTIC,
                                TransactionIsolation.SERIALIZABLE)) {
                            walletPoJo = walletCache.get(userPoJo.getWalletId());
                            assert walletPoJo != null;
                            //更新钱包余额。
                            walletPoJo.setBalance(walletPoJo.getBalance() - amount);

                            //创建钱包余额变动记录。
                            WalletChangeDetailPoJo changeDetail = WalletChangeDetailPoJo.builder()
                                    .id(IdSnowFlake.getId())
                                    .eventId(history.getId())
                                    .eventType(WalletEventType.CASH_WITHDRAWAL.name())
                                    .amount(-request.getAmount())
                                    .balance(walletPoJo.getBalance())
                                    .walletId(walletPoJo.getId())
                                    .creatorId(userPoJo.getId())
                                    .version(0L)
                                    .createTime(new Timestamp(System.currentTimeMillis()))
                                    .insert(true)
                                    .build();
                            //创建提现手续费扣费记录。
                            WalletChangeDetailPoJo changeDetail1 = WalletChangeDetailPoJo.builder()
                                    .id(IdSnowFlake.getId())
                                    .eventId(history.getId())
                                    .eventType(WalletEventType.CASH_WITHDRAWAL_FEE.name())
                                    .amount(-fee)
                                    .balance(walletPoJo.getBalance())
                                    .walletId(walletPoJo.getId())
                                    .creatorId(userPoJo.getId())
                                    .version(0L)
                                    .createTime(new Timestamp(System.currentTimeMillis()))
                                    .insert(true)
                                    .build();
                            //保存资金变动记录。
                            walletDetailCache.put(changeDetail.getKey(), changeDetail);
                            walletDetailCache.put(changeDetail1.getKey(), changeDetail1);
                            walletCache.put(walletPoJo.getKey(), walletPoJo);
                            //保存提现历史记录。
                            withdrawHistoryCache.put(history.getKey(), history);
                            tx.commit();
                        }
                        return ExecuteResult.success("操作成功", true);
                    }

                    return ExecuteResult.failure(error.toString(), false);

                } catch (Exception ex) {
                    return ExecuteResult.failure(ex.getMessage(), false);
                }
            } else {
                return ExecuteResult.failure("系统繁忙，请稍后再试", false);
            }
        } catch (Exception ex) {
            return ExecuteResult.failure(ex.getMessage(), false);
        } finally {
            try {
                if (fileData != null) {
                    fileData.close();
                }
            } catch (Exception ex) {
            }
        }
    }

    public Long asyncConfirmWithdraw(HttpServletRequest httpServletRequest) {
        ConfirmWithdrawRequest request = convertFrom(httpServletRequest);

        String sql = MessageFormat.format("select * from {0} where merchantNum = ? and orderNo = ?",
                RechargeHistoryPoJo.CACHE_NAME);
        WithdrawHistoryPoJo history = IgniteQueryHelper.findOneBy(sql, new Object[]{request.getMerchantNum(),
                request.getOrderNo()}, WithdrawHistoryPoJo.class, withdrawHistoryCache);
        if (history == null || history.getConfirmMode() != 0 || Boolean.TRUE.equals(history.getSyncBalance())) {
            return null;
        }

        //核对签名。
        String strSign = request.getState() + request.getMerchantNum() + request.getOrderNo() + request.getAmount()
                + payProperties.getMerchantSecretKey();
        String sign = DigestUtils.md5Hex(strSign);
        if (!request.getSign().equals(sign)) {
            log.error("----asyncConfirmWithdraw------{}:{}", "签名不一致", strSign);
            return null;
        }

        try (IgniteSemaphore withdrawSemaphore = ignite.semaphore("confirmWithdraw:" + history.getId(), 1, true, true)) {
            if (withdrawSemaphore.tryAcquire(3, TimeUnit.SECONDS)) {
                //再次获取提现历史记录。
                history = IgniteQueryHelper.findOneBy(sql, new Object[]{request.getMerchantNum(), request.getOrderNo()},
                        WithdrawHistoryPoJo.class, withdrawHistoryCache);
                if (history == null || history.getConfirmMode() != 0 || Boolean.TRUE.equals(history.getSyncBalance())) {
                    return null;
                }

                BigDecimal amount = new BigDecimal(request.getAmount());
                long actualAmount = amount.multiply(BigDecimal.valueOf(100L)).longValue();

                //回填提现记录上的值。
                history.setConfirmMode(1);
                history.setState(Integer.parseInt(request.getState()));
                history.setActualAmount(actualAmount);
                history.setWithdrawTime(new Timestamp(System.currentTimeMillis()));
                //保存数据。
                withdrawHistoryCache.put(history.getKey(), history);
            }
        } catch (Exception ex) {
        }
        return history.getId();
    }

    public void updateWalletBalance(Long withdrawHistoryId) {
        WithdrawHistoryPoJo history = withdrawHistoryCache.get(withdrawHistoryId);
        if (history == null || Boolean.TRUE.equals(history.getSyncBalance()) || history.getConfirmMode() == 0) {
            return;
        }

        IMUserPoJo userPoJo = userCache.get(Long.parseLong(history.getUserId()));
        assert userPoJo != null;

        try (IgniteSemaphore walletSemaphore = ignite.semaphore("wallet:" + userPoJo.getWalletId(), 1, true, true)) {
            if (walletSemaphore.tryAcquire(3, TimeUnit.SECONDS)) {
                try {
                    //再获取一次提现历史记录。
                    history = withdrawHistoryCache.get(withdrawHistoryId);
                    if (history == null || Boolean.TRUE.equals(history.getSyncBalance()) || history.getConfirmMode() == 0) {
                        return;
                    }

                    try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.OPTIMISTIC,
                            TransactionIsolation.SERIALIZABLE)) {

                        history.setSyncBalance(true);
                        if (history.getState() != 4) {
                            //回退提现金额。
                            String sql = MessageFormat.format("select * from {0} where eventId = ? and (eventType = ? or eventType = ?)",
                                    WalletChangeDetailPoJo.CACHE_NAME);
                            List<WalletChangeDetailPoJo> changeDetailPoJos = IgniteQueryHelper.findAllBy(sql,
                                    new Object[]{history.getId(), WalletEventType.CASH_WITHDRAWAL.name(), WalletEventType.CASH_WITHDRAWAL_FEE.name()},
                                    WalletChangeDetailPoJo.class, walletDetailCache);
                            if (CollectionUtils.isEmpty(changeDetailPoJos)) {
                                return;
                            }

                            WalletPoJo wallet = walletCache.get(userPoJo.getWalletId());
                            assert wallet != null;

                            Map<Long, WalletChangeDetailPoJo> detailPoJoMap = new HashMap<>();
                            for (WalletChangeDetailPoJo changeDetailPoJo : changeDetailPoJos) {
                                //将扣减的金额退回钱包。
                                wallet.setBalance(wallet.getBalance() - changeDetailPoJo.getAmount());
                                //创建资金回退记录。
                                WalletChangeDetailPoJo detailPoJo = new WalletChangeDetailPoJo();
                                BeanUtils.copyProperties(changeDetailPoJo, detailPoJo);
                                detailPoJo.setId(IdSnowFlake.getId());
                                detailPoJo.setEventType("ROLLBACK_" + detailPoJo.getEventType());
                                detailPoJo.setAmount(-changeDetailPoJo.getAmount());
                                detailPoJo.setBalance(wallet.getBalance());
                                detailPoJo.setVersion(0L);
                                detailPoJo.setInsert(true);
                                detailPoJo.setCreateTime(new Timestamp(System.currentTimeMillis()));
                                detailPoJoMap.put(detailPoJo.getKey(), detailPoJo);
                            }
                            walletCache.put(wallet.getKey(), wallet);
                            walletDetailCache.putAll(detailPoJoMap);
                        }
                        withdrawHistoryCache.put(history.getKey(), history);
                        tx.commit();
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public boolean queryWithdrawResult(Long withdrawId, StringBuilder error) {
        WithdrawHistoryPoJo historyPoJo = withdrawHistoryCache.get(withdrawId);
        if (WithdrawHelper.queryWithdrawResult(historyPoJo, restTemplate, payProperties, error)) {
            historyPoJo.setInsert(false);
            withdrawHistoryCache.put(historyPoJo.getKey(), historyPoJo);
            return true;
        }
        return false;
    }

    private String getNotifyUrlPrefix() {
        WithdrawController controller = applicationContext.getBean(WithdrawController.class);
        RequestMapping ann = null;
        if (controller.getClass().getSimpleName().endsWith(".WithdrawController")) {
            ann = controller.getClass().getAnnotation(RequestMapping.class);
        } else {
            ann = ((Class<?>) controller.getClass().getGenericSuperclass()).getAnnotation(RequestMapping.class);
        }
        assert ann != null;

        String uri = ann.value()[0];
        uri = uri.replace("${api-url-prefix}", apiUrlPrefix);
        return "http://" + serviceProperties.getExternalWalletService() + uri + "/confirm/async";
    }

    private WithdrawBindingParamPoJo getBindingParam(Long currentUserId, ChannelType type) {
        String sql = MessageFormat.format("select * from {0} where creatorId = ? and channelType = ?",
                WithdrawBindingParamPoJo.CACHE_NAME);
        return IgniteQueryHelper.findOneBy(sql, new Object[]{currentUserId, type.name()}, WithdrawBindingParamPoJo.class, withdrawParamCache);
    }

    private void configChannelParam(ChannelType channelType, WithdrawBindingParamPoJo paramPoJo, WithdrawHistoryPoJo history) {
        history.setChannelCode(channelType.getType());
        switch (channelType) {
            case WE_CHAT -> {
                history.setAccountHolder(paramPoJo.getAccountHolder());
            }
            case ALI_PAY -> {
                history.setBankCardAccount(paramPoJo.getBankCardAccount());
                history.setAccountHolder(paramPoJo.getAccountHolder());
            }
            default -> {
                history.setBankCardAccount(paramPoJo.getBankCardAccount());
                history.setOpenAccountBank(paramPoJo.getOpenAccountBank());
                history.setAccountHolder(paramPoJo.getAccountHolder());
            }
        }
    }

    private InputStream getFileData(String fileUri) {
        InputStream dataStream = null;
        try {
            String[] infos = fileUri.split("/");
            //获取用户头像文件访问路径。
            String fileUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(this.bucket)
                    .object("file/" + infos[infos.length - 1])
                    .build());

            String formatName = "png";
            infos = fileUri.split("\\.");
            if (infos.length > 1) {
                formatName = infos[infos.length - 1];
            }

            assert fileUrl != null;
            URL url = new URL(fileUrl);
            BufferedImage bufferedImage = ImageIO.read(url);
            if (bufferedImage != null) {
                //是图片文件。
                try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                    ImageIO.write(bufferedImage, formatName.toUpperCase(), outputStream);
                    dataStream = new ByteArrayInputStream(outputStream.toByteArray());
                }
            }
        } catch (Exception ex) {
        }
        return dataStream;
    }

    private ConfirmWithdrawRequest convertFrom(HttpServletRequest req) {
        ConfirmWithdrawRequest request = new ConfirmWithdrawRequest();
        Field[] fields = request.getClass().getDeclaredFields();
        for (Field field : fields) {
            try {
                String val = req.getParameter(field.getName());
                log.error("---{}={}---", field.getName(), val);
                field.setAccessible(true);
                field.set(request, val);
            } catch (Exception ex) {
            }
        }
        return request;
    }
}
